using System;
using System.Collections.Generic;
using System.Text;
using System.IO.Ports;
using System.IO;

namespace DigitalTrainSuite
{
	public delegate void AppDataReady();
	public delegate void RS485Error(string message);
	public delegate void NetworkMessage(string message);

	[global::System.Serializable]
	public class SensorInterfaceNotRespondingException : IOException
	{
		//
		// For guidelines regarding the creation of new exception types, see
		//    http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpgenref/html/cpconerrorraisinghandlingguidelines.asp
		// and
		//    http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dncscol/html/csharp07192001.asp
		//

		public SensorInterfaceNotRespondingException() { }
		public SensorInterfaceNotRespondingException(string message) : base(message) { }
		public SensorInterfaceNotRespondingException(string message, Exception inner) : base(message, inner) { }
		protected SensorInterfaceNotRespondingException(
		  System.Runtime.Serialization.SerializationInfo info,
		  System.Runtime.Serialization.StreamingContext context)
			: base(info, context) { }
	}

	public struct SensorPacket
	{
		public byte PRE;
		public byte DES;
		public byte SRC;
		public byte FLG;
		public byte GRP;
		public byte LEN;
		public byte[] DAT;
		public byte CHK;

		public bool isGroup;
		public bool isPing;
		public bool isStartup;
		public bool isWarning;
		public bool isResponse;

		public int groupNumber;
		public int packetNumber;
		public int length;
		public int destination;
		public int source;

		public SensorPacket(bool isGroup,
							bool isPing,
							bool isStartup,
							bool isWarning,
							bool isResponse,
							int groupNumber,
							int packetNumber,
							int length,
							int destination,
							int source,
							byte[] data)
		{
			this.isGroup = isGroup;
			this.isPing = isPing;
			this.isStartup = isStartup;
			this.isWarning = isWarning;
			this.isResponse = isResponse;
			this.groupNumber = groupNumber;
			this.packetNumber = packetNumber;
			this.length = length;
			this.destination = destination;
			this.source = source;

			this.PRE = 0xA3;
			this.DES = (byte)destination;
			this.SRC = (byte)source;
			this.FLG = (byte)((isGroup ? (byte)0x01 : (byte)0) |
								(isPing ? (byte)0x80 : (byte)0) |
								(isStartup ? (byte)0x40 : (byte)0) |
								(isWarning ? (byte)0x20 : (byte)0) |
								(isResponse ? (byte)0x10 : (byte)0)
								);
			this.GRP = (byte)((((groupNumber & 0x0f) << 4) | (packetNumber & 0x0f)) & 0xff);
			this.LEN = (byte)length;
			if (data != null)
			{
				this.DAT = new byte[data.Length];
				for (int index = 0; index < data.Length; index++)
				{
					this.DAT[index] = (byte)data[index];
				}
			}
			else
			{
				this.DAT = null;
			}

			this.CHK = 0;
			if (this.DAT != null)
			{
				foreach (byte ch in data)
				{
					this.CHK += ch;
				}
			}
            this.CHK += this.PRE;
			this.CHK += this.DES;
			this.CHK += this.SRC;
			this.CHK += this.FLG;
			this.CHK += this.GRP;
			this.CHK += this.LEN;
		}



		public byte[] toByteArray()
		{
			byte [] bytes = new byte[this.length + 6 + (this.isGroup ? 1 : 0)];

			bytes[0] = this.PRE;
			bytes[1] = this.DES;
			bytes[2] = this.SRC;
			bytes[3] = this.FLG;
			if (this.isGroup)
			{
				bytes[4] = this.GRP;
                bytes[5] = this.LEN;
				if (this.DAT != null) this.DAT.CopyTo(bytes, 6);
			}
			else
			{
                bytes[4] = this.LEN;
				if (this.DAT != null) this.DAT.CopyTo(bytes, 5);
			}
			bytes[bytes.Length - 1] = this.CHK;

			return bytes;
		}
	}

	public enum State { PRE, DES, SRC, FLG, GRP, LEN, DAT, CHK }

	class RS485Driver : IDisposable
	{
		private string portName;
		private int baud;
		public SerialPort port;
		private Queue<SensorPacket> toParse;
		private Queue<SensorPacket> toSend;
		public Queue<byte[]> toApp;

		private bool connected = false;

		private delegate void PacketReceived();
		
		
		#region public events

		public event RS485Error recieveError;
		public event NetworkMessage networkMessage;
		public event AppDataReady appDataReady;

		#endregion

		#region private events

		private event PacketReceived packetEnqueue;

		#endregion

		#region constructors

		public RS485Driver()
		{
			this.RS485Driver_("COM3", 9600);
		}

		public RS485Driver(string COMport)
		{
			this.RS485Driver_(COMport, 9600);
		}

		public RS485Driver(string COMport, int baud)
		{
			this.RS485Driver_(COMport, baud);
		}

		public RS485Driver(string COMport, int baud, AppDataReady callback)
		{
			this.RS485Driver_(COMport, baud);
			this.appDataReady += callback;
		}

		private void RS485Driver_(string COMport, int baud)
		{
			this.portName = COMport;
			this.baud = baud;
			this.toApp = new Queue<byte[]>();
			this.toParse = new Queue<SensorPacket>();
			this.toSend = new Queue<SensorPacket>();
			try
			{
				this.port = new SerialPort(portName, baud, Parity.None, 8, StopBits.One);
			}
			catch (Exception e)
			{
				System.Console.Error.WriteLine(e.Message);
			}
			if (port == null) throw new IOException(String.Format("Error opening {0} port", portName));
			this.port.Handshake = Handshake.RequestToSend;
			//this.port.ReadTimeout = 200;
			//this.port.WriteTimeout = 200;
		}

		#endregion

		public void start()
		{
			try
			{
				this.port.Open();
				this.testConnection(port);
			}
			catch (Exception e)
			{
				this.port.Close();
				throw e;
			}

			this.port.DataReceived += new SerialDataReceivedEventHandler(this.port_DataReceived);
			this.packetEnqueue += new PacketReceived(RS485Driver_packetReceived);
		}


		/// <summary>
		/// Event handler for when a packet is on the queue to be parsed
		/// </summary>
		void  RS485Driver_packetReceived()
		{
			SensorPacket packet;

			if (this.toParse.Count < 1) return;
 			packet = this.toParse.Dequeue();
			//System.Console.Out.WriteLine("After queue check in RS485Driver_packetReceived");

			//preparse the packet into typed data
			if (!RS485Driver.preParsePacket(ref packet)) return;
			//System.Console.Out.WriteLine("After preparse in RS485Driver_packetReceived");

			//Packet is a part of a group of packets
			if (packet.isGroup) throw new Exception("Group packets are not implemented yet.");
			//Packet is a ping
			if (packet.isPing)
			{
				if (packet.isResponse)
				{
					if (this.networkMessage != null) this.networkMessage(String.Format("Ping Response from: {0}", packet.source));
				}
				else
				{
					if (this.networkMessage != null) this.networkMessage(String.Format("Ping Request Received from: {0}", packet.source));
					SensorPacket sendpacket = new SensorPacket(false, true, false, false, true, 0, 0, 0, packet.source, 0, null);
					this.send(sendpacket);
					return;
				}
			}
			//Packet is a startup packet
			else if (packet.isStartup)
			{
				if (packet.isWarning)	//Warning in startup
				{
					if (this.networkMessage != null) this.networkMessage(String.Format("Warning at startup from: {0}", packet.source));
				}
				else  //Regular startup packet
				{
					if (this.networkMessage != null) this.networkMessage(String.Format("Startup from: {0}", packet.source));
				}
			}
			//Packet contains a warning
			else if (packet.isWarning)
			{
				if (this.networkMessage != null) this.networkMessage(String.Format("Warning from: {0}", packet.source));
			}
			//packet contains a response
			else if (packet.isResponse)
			{
				if (this.networkMessage != null) this.networkMessage(String.Format("Response from: {0}\n {1}", packet.source, packet.DAT));
			}
			else
			{
				this.toApp.Enqueue(packet.DAT);
			}
			this.appDataReady();
			return;
		}

		/// <summary>
		/// Runs the checksum, and converts the bytes to the typed values.
		/// </summary>
		/// <param name="packet">The packet to be preParsed</param>
		/// <returns>true if the checksum was correct</returns>
		private static bool preParsePacket(ref SensorPacket packet)
		{
			byte sum;

			//System.Console.Out.WriteLine(packet.PRE.ToString());
			//System.Console.Out.WriteLine(packet.DES.ToString());
			//System.Console.Out.WriteLine(packet.SRC.ToString());
			//System.Console.Out.WriteLine(packet.FLG.ToString());
			//System.Console.Out.WriteLine(packet.GRP.ToString());
			//System.Console.Out.WriteLine(packet.LEN.ToString());
			//System.Console.Out.WriteLine(packet.DAT.ToString());
			//System.Console.Out.WriteLine(packet.CHK.ToString());

			//sum up the bytes for the checksum
			sum = (byte)(packet.PRE + packet.DES + packet.SRC + packet.FLG + packet.GRP + packet.LEN);
			for (int i = 0; i < packet.LEN; i++)
			{
				sum += packet.DAT[i];
			}

			//System.Console.Out.WriteLine(sum.ToString());

			if (sum != packet.CHK)
				return false;

			//Set the typed values of the packet
			packet.isPing = (packet.FLG & 0x80) > 0;
			packet.isStartup = (packet.FLG & 0x40) > 0;
			packet.isWarning = (packet.FLG & 0x20) > 0;
			packet.isResponse = (packet.FLG & 0x10) > 0;
			packet.isGroup = (packet.FLG & 0x01) > 0;

			//set other numerical values of the packet
			packet.groupNumber = (packet.GRP & 0xf0) >> 4;
			packet.packetNumber = (packet.GRP & 0x0f);
			packet.length = (packet.LEN);
			packet.source = (packet.SRC);
			packet.destination = packet.DES;

			return true;
		}


		// Tests writing to the port
		private void testConnection(SerialPort port)
		{
			/*byte[] buf = new byte[3];

			buf[0] = 0;
			buf[1] = 0;
			buf[2] = 0;

			try
			{
				port.Write(buf, 0, 3);
			}
			catch (TimeoutException e)
			{
				throw new SensorInterfaceNotRespondingException("Cannot write to " + port.PortName + "\n" + e.Message);
			}*/
		}


		#region send functions

		private void send(SensorPacket packet)
		{
			try
			{
				byte[] bytes = packet.toByteArray();

				//for (int i=0; i<bytes.Length; i++)
				//{
				//	System.Console.Out.WriteLine("Byte Sent: {0}", Convert.ToString(bytes[i]));
					
					//this.port.Write(bytes, i, 1);
					//System.Threading.Thread.Sleep(1);
				//}

				this.port.Write(bytes, 0, bytes.Length);
			}
			catch (IOException e)
			{
				System.Console.Error.WriteLine(e.Message);
			}
		}

		public bool send(int sensorId, byte[] data)
		{
			try
			{
				SensorPacket packet = new SensorPacket(false, false, false, false, false, 0, 0, data.Length, sensorId, 0, data);
				this.send(packet);
			}
			catch (IOException e)
			{
				System.Console.Error.WriteLine(e.Message);
			}

			return true;
		}

		public bool send(int sensorId, byte[] data, bool isWarning, bool isResponse)
		{
			try
			{
				SensorPacket packet = new SensorPacket(false, false, isWarning, isResponse, false, 0, 0, data.Length, sensorId, 0, data);
				this.send(packet);
			}
			catch (IOException e)
			{
				System.Console.Error.WriteLine(e.Message);
			}

			return false;
		}

		public bool ping(int sensorId)
		{
			try
			{
				SensorPacket packet = new SensorPacket(false, true, false, false, false, 0, 0, 0, sensorId, 0, null);
				this.send(packet);
			}
			catch (IOException e)
			{
				System.Console.Error.WriteLine(e.Message);
			}

			return false;
		}

		public bool pingAll()
		{
			try
			{
				SensorPacket packet = new SensorPacket(false, true, false, false, false, 0, 0, 0, 0xFF, 0, null);
				this.send(packet);
			}
			catch (IOException e)
			{
				System.Console.Error.WriteLine(e.Message);
			}

			return false;
		}

		public bool startup(bool warning, byte[] message)
		{
			try
			{
				SensorPacket packet = new SensorPacket(false, false, true, warning, false, 0, 0, ((message == null)?0:message.Length), 0xFF, 0, message);
				this.send(packet);
				return true;
			}
			catch (IOException e)
			{
				System.Console.Error.WriteLine(e.Message);
			}

			return false;
		}

		#endregion


		private SensorPacket currentPacket;
		private State state = State.PRE;
		private int dataLeft = 0;

		/// <summary>
		/// Function to receive SerialDataReceived events.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		void port_DataReceived(object sender, SerialDataReceivedEventArgs e)
		{
			byte input = 0;
			int bytesRead;
			
			try
			{
				while (this.port.BytesToRead > 0)
				{
					
					if (this.state == State.DAT)
					{
						bytesRead = this.port.Read(this.currentPacket.DAT, this.currentPacket.LEN-this.dataLeft, this.dataLeft);
						this.dataLeft = this.dataLeft - bytesRead;
					}
					else
					{
						input = (byte)this.port.ReadByte();
						System.Console.Out.WriteLine("Byte Read: " + input);
					}

					//System.Console.Out.WriteLine("Byte Received 0x{0:x}", input);

					switch (state)
					{
						case State.PRE:
							if (input != 0xA3) break;
							this.currentPacket = new SensorPacket();
							this.currentPacket.PRE = input;
							this.state = State.DES;
							break;
						case State.DES:
							this.currentPacket.DES = input;
							this.state = State.SRC;
							break;
						case State.SRC:
							this.currentPacket.SRC = input;
							this.state = State.FLG;
							break;
						case State.FLG:
							this.currentPacket.FLG = input;
							if ((input & 0x01) > 0) this.state = State.GRP;
							else
							{
								this.state = State.LEN;
								this.currentPacket.GRP = 0;
							}
							break;
						case State.GRP:
							this.currentPacket.GRP = input;
							this.state = State.LEN;
							break;
						case State.LEN:
							this.currentPacket.LEN = input;
							this.dataLeft = input;
							this.currentPacket.DAT = new byte[this.dataLeft];
							this.state = State.DAT;
							break;
						case State.DAT:
							if (this.dataLeft > 0) break;
							else this.state = State.CHK;
							break;
						case State.CHK:
							this.currentPacket.CHK = input;
							this.toParse.Enqueue(this.currentPacket);
							this.packetEnqueue();
							this.state = State.PRE;
							break;
						default:
							throw new Exception("Invalid state in RS485 Driver packet receiver");
					}


				}
			}
			catch (InvalidOperationException ioe)
			{
				System.Console.Error.WriteLine(ioe.Message);
			}
			catch (IOException ioe)
			{
				System.Console.Error.WriteLine(ioe.Message);
			}
			catch (TimeoutException ioe)
			{
				System.Console.Error.WriteLine(ioe.Message);
				if (this.recieveError != null)
				{
					this.recieveError(ioe.Message);
				}
			}
		}


		#region IDisposable Members

		public void Dispose()
		{
			if (this.port.IsOpen) this.port.Close();
			this.connected = false;
		}

		#endregion
	}
}
